<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>主应用</h1>
    <script type="systemjs-importmap">
      {
        "imports": {
          "react": "https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.development.js",
          "react-dom": "https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"
        }
      }
    </script>
    <div id="root"></div>
    <!-- <script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.15.1/system.min.js"></script> -->
    <script>
      class SystemJs {
        import(id) {
          // id 是资源的路径，可以是第三方cdn资源路径
          return Promise.resolve()
            .then(() => {
              // 1. 解析importMap
              this.#processScript();
              // 2. 去当前路径去找对应资源路径
              // http://127.0.0.1:5500/mini-systemjs/main/main.html，找到最后一个 / 的位置，前面的就是路径
              const lastSepIndex = location.href.lastIndexOf("/");
              // http://127.0.0.1:5500/mini-systemjs/main/
              const baseUrl = location.href.slice(0, lastSepIndex + 1);
              // 当前是模拟本地路径，所以这里“写死了“
              if (id.startsWith("../dist")) {
                return baseUrl + id;
              }
            })
            .then((id) => {
              // 这里的id就是上个Promise返回的baseUrl + id
              /**
               *        id: http://127.0.0.1:5500/mini-systemjs/main/../dist/index.js，
               * 浏览器会转为 http://127.0.0.1:5500/mini-systemjs/dist/index.js
              */ 
              // 根据文件路径加载资源
              return this.#load(id);
            }).then((register) => {
              // register: [['react', 'react-dom'], function() {}] 是在load执行完成后，通过resolve传到这里的
              const [deps, declare] = register;
              /**
               * 因为这个 declare 为 function(__WEBPACK_DYNAMIC_EXPORT__, __system_context__) {
               *    return { setters, execute }
               * }
               * setters：是用来保存加载后的资源，打包内容中webpack采用变量替换了
               * execute：是真正资源加载逻辑
              */
              const { setters, execute } = declare(() => {}, {})
              Promise.all(deps.map((dep, i) => {
                return this.#load(dep).then(() => {
                  // setters: [function(module) {}] 会传入 module 参数
                  // cdn加载完毕后，会在window上添加属性，例如：window.React
                  const property = this.#getLastGlobalProperty()
                  setters[i](property)
                })
              })).then(() => {
                // 当对应变量替换好后，执行具体渲染逻辑
                execute();
              })
            })
        }

        #mapUrl = {};
        // 解析importMap
        #processScript() {
          Array.from(document.querySelectorAll("script")).forEach((script) => {
            if (script.type === "systemjs-importmap") {
              const imports = JSON.parse(script.innerHTML).imports;
              Object.entries(imports).forEach(([key, value]) => {
                this.#mapUrl[key] = value;
              });
            }
          });
        }

        // 加载资源
        #load(id) {
          return new Promise((resolve, reject) => {
            // 通过script来获取资源，不用fetch是因为fetch会跨域
            const script = document.createElement("script");
            script.src = this.#mapUrl[id] || id; // this.#mapUrl[id] 去映射表查找url资源，这样就可以支持cdn了
            script.async = true;
            // 此时会去执行脚本，在打包的文件中，有System.register方法去执行，接下来要去书写register方法
            document.head.appendChild(script);
            script.onload = () => {
              let _lastRegister = this.#lastRegister;
              this.#lastRegister = undefined;
              resolve(_lastRegister);
            };
          })
        }

        constructor() {
          this.#saveGlobalProperty();
        }

        #set = new Set();
        // 保存之前的window属性（快照）
        #saveGlobalProperty() {
          for(let k in window) {
            this.#set.add(k);
          }
        }
        // 获取window上最后添加的属性（新增属性，cdn导入后，会在window上添加属性）
        #getLastGlobalProperty() {
          for(let k in window) {
            if(this.#set.has(k)) continue;
            this.#set.add(k);
            return window[k];
          }
        }

        /**
         * @param {string[]} deps 依赖列表
         * @param {function} declare 声明函数
        */
        register(deps, declare) {
          // 为了后续在load使用，保存在变量中
          // 等文件加载完成后，load中会将结果传递到下一个Promise中
          this.#lastRegister = [deps, declare]
        }

        #lastRegister
      }

      const System = new SystemJs;

      // 直接加载子应用（异步的）
      System.import("../dist/index.js").then(() => {
        console.log("模块加载完毕");
      });
    </script>
  </body>
</html>
